require 'net/smtp'

# Symbol extensions
class Symbol
  # Returns the symbol as a header string
  def to_header
    to_s.split('_').map{|w| w.capitalize}.join('-')
  end
end

class String
  # Returns self
  def to_header
    self
  end
  
  # Returns the address part of a recipient
  def email_addr
    s = strip
    return nil if s.empty?
    s =~ /<(.*)>/ ? $1 : s
  end
end

class Array
  # Splits the array into smaller arrays of fixed size
  def split(batch)
    result = []
    idx = 0
    while idx < size
      result << slice(idx, batch)
      idx += batch
    end
    result
  end
end

class BulkMail
  @@server_settings = {
    :host => 'localhost',
    :port => 25,
    :helo_domain => nil,
    :user => nil,
    :password => nil,
    :auth => :plain
  }

  # Returns the server_settings hash
  def self.server_settings
    @@server_settings
  end
  
  LINE_BREAK = "\r\n"
  HEADER_FMT = "%s: %s\r\n".freeze
  
  # Returns the message headers formatted
  def self.format_headers(headers)
    headers.inject('') do |m, kv|
      m << (HEADER_FMT % [kv[0].to_header, kv[1]])
    end
  end
  
  # Starts an SMTP session and yields
  def self.smtp
    Net::SMTP.start(
      @@server_settings[:host], 
      @@server_settings[:port], 
      @@server_settings[:helo_domain] || @@server_settings[:host],
      @@server_settings[:user], 
      @@server_settings[:password], 
      @@server_settings[:auth]) do |smtp|
      yield smtp
    end
  end
  
  # Sends an email
  def self.send_message(smtp, from, to, body, headers = nil)
    if headers && !headers.empty?
      msg = format_headers(headers)
    else
      msg = LINE_BREAK
    end
    msg << LINE_BREAK
    msg << body
    smtp.send_message msg, from, to
  end
  
  # Sends bulk mail to a large number of recipients.
  def self.send_bulk(sender, recipients, body, headers)
    failed = []
    succeeded = []
    headers[:from] = headers[:to] = sender
    recipients = recipients.map {|r| r.email_addr}.compact
    recipients.split(10).each do |list|
      begin
        smtp {|s| send_message(s, sender.email_addr, list, body, headers)}
        succeeded.concat list
        puts "Sent message to #{list.join(', ')}"
      rescue => e
        failed.concat list
        puts "Failed to send to #{list.join(', ')}: #{e.message}\r\n#{e.backtrace.first}"
      end
    end
    # log failed addresses
    unless failed.empty?
      File.open('failed.bulkmail', 'w') {|f| failed.each {|l|f.puts l}}
    end
    puts "Message to sent to #{succeeded.size} of #{recipients.size} recipients"
  end
end
